package com.googlecode.cswish.struts.conversion;

import java.io.CharArrayWriter;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.views.freemarker.FreemarkerManager;
import org.apache.struts2.views.freemarker.ScopesHashModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import com.googlecode.cswish.Config;
import com.googlecode.cswish.model.PageElements;
import com.googlecode.cswish.security.SafeManager;
import com.googlecode.cswish.security.auth.UserInfo;
import com.googlecode.cswish.struts.BaseActionSupport;
import com.googlecode.cswish.struts.ContextHolder;
import com.googlecode.cswish.struts.DynamicAction;
import com.googlecode.cswish.struts.ITaglibManager;
import com.googlecode.cswish.struts.Invoker;
import com.googlecode.cswish.struts.Routing;
import com.googlecode.cswish.struts.spring.BeanIntrospector;
import com.googlecode.cswish.struts.spring.CacheService;
import com.googlecode.cswish.struts.spring.CacheService.Wrapper;
import com.googlecode.cswish.struts.spring.Executor;
import com.googlecode.cswish.struts.spring.GenericService;
import com.googlecode.cswish.struts.spring.ObjectFactory;
import com.googlecode.cswish.util.FileUtil;
import com.googlecode.cswish.util.FrameConstant;
import com.googlecode.cswish.util.Functions;
import com.googlecode.cswish.util.InvocationUtil;
import com.googlecode.cswish.util.InvocationUtil.ServiceInfo;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.util.ValueStack;

import freemarker.core.Environment;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateModelException;

@Service
public class PageElementService {

	protected static Logger logger = LoggerFactory.getLogger(PageElementService.class);
	
	private Map<String, PageElementsHolder> cache;

	@Inject
	protected FreemarkerManager freemarkerManager;
		
	@Resource
	protected ObjectFactory objectFactory;

	@Resource
	private Routing routing;
	
	@Resource
	private SafeManager safeManager;
	
	@Resource
	private BeanIntrospector beanIntrospector;
	
	@Resource
	private Functions functions;
	
	@Resource
	private StrutsFreemarkerManager freemarkerTplManager;
	
	@Resource
	private ITaglibManager taglibManager;
	
	@Resource
	private GenericService genericService;
	
	@Resource
	private Config config;
	
	@Resource
	private CacheService cacheService;
	
	@Resource
	private InvocationUtil invocationUtil;
	
	@Resource
	private FileUtil fileUtil;
	
	private Wrapper<Properties> pageElementProperties;
	
	@PostConstruct
	private void init() {
		cache = cacheService.newCache(PageElementsHolder.class);
		
		functions.registCommonService("ps", this);
	}
	
	class PageElementsRef {
		PageElements pageElements;
		PageElementsHolder ref;
		
		public PageElementsRef(PageElements pageElements, PageElementsHolder ref) {
			this.pageElements = pageElements;
			this.ref = ref;
		}
	}
	
	class PageElementsHolder {
		PageElements pageElements;
		String location;
		boolean valid;
		long cacheTime;
		
		public PageElementsHolder(String location, PageElements pageElements) {
			this.location = location;
			this.pageElements = pageElements;
			this.valid = true;
			this.cacheTime = System.currentTimeMillis();
		}
	}
	
	/**
	 * Get the user's page elements
	 * 
	 * @param className
	 * @param method
	 * @return
	 */
	public PageElements getPageElements(String className, String method) {
		ActionContext context = ActionContext.getContext();
		
		PageElements pageElements;
		if (context != null && context.getActionInvocation() != null) {
			ActionInvocation invocation = context.getActionInvocation();
			String simpleModelName = beanIntrospector.getSimpleName(className);
	
			pageElements = getPageElements(invocation, simpleModelName, method, null, true);
		} else {
			pageElements = new PageElements();
		}
		return pageElements;
	}
	
	public PageElements getCurrentPageElements(boolean mustInputMethod) {
		ActionContext context = ActionContext.getContext();
		PageElements ps = null;
		if (context != null) {
			ActionInvocation invocation = context.getActionInvocation();
			if (invocation != null) {
				String actionName = (String) invocation.getStack().getContext().get(FrameConstant.STACK_ACTIONNAME);
				if (actionName != null) {
					Invoker invoker = routing.action2Invoker(actionName);
					String simpleModelName = beanIntrospector.getSimpleName(actionName);
					String methodName = invoker.methodName;
					if (mustInputMethod && methodName != null && !methodName.startsWith(FrameConstant.ACTION_PREPARE)) {
						methodName = FrameConstant.ACTION_PREPARE + methodName;
					}
					
					// see PageElementInterceptor.intercept
					Object resultCode = invocation.getInvocationContext().getParameters().get(FrameConstant.PARAM_RESULTCODE);
					String strResultCode = null;
			    	if (resultCode != null && resultCode instanceof String[]) {
						String[] resultCodes = (String[]) resultCode;
						// NOTICE: '$element' share the original PageElements
						if (resultCodes.length > 0 && !resultCodes[0].equals(FrameConstant.PARAM_RESULT_ELEMENT)) {
							strResultCode = resultCodes[0];
						}
			    	}
					ps = getPageElements(invocation, simpleModelName, methodName, strResultCode, mustInputMethod);
				}
			} 
		}
		if (ps == null){
			ps = new PageElements();
		}
		return ps;
	}
	
	public PageElements getPageElements(ActionInvocation invocation, final String simpleModelName, final String method, 
			String resultCode, boolean autoCreation) {
		Object inputCode = invocation.getInvocationContext().getParameters().get(FrameConstant.PARAM_INPUTCODE);
		String strInputCode = inputCode == null ? null : ((String[])inputCode)[0];
		String key = getKey(simpleModelName, method, resultCode, strInputCode);
		Executor<String, Object> lazyPath = new Executor<String, Object>() {
			@Override
			public String execute(Object parameters) {
				return getPathByModelMethod(simpleModelName, method, false);
			}
		};
		String context = Routing.invoker2PathNoExtension(simpleModelName, '.', method, true);
		return getPageElementsByPath(invocation, key, context, lazyPath, autoCreation, resultCode, strInputCode);
	}
	
	public PageElements getTemplatePageElements(ActionInvocation invocation, final String path) {
		Executor<String, Object> lazyPath = new Executor<String, Object>() {
			@Override
			public String execute(Object parameters) {
				return path;
			}
		};
		return getPageElementsByPath(invocation, path, null, lazyPath, true, null, null);
	}
	
	public PageElements getPageElementsNoPermissionFilter(String className, String method) {
		ActionContext context = ActionContext.getContext();
		ActionInvocation invocation = context.getActionInvocation();
		String simpleModelName = beanIntrospector.getSimpleName(className);
		String key = getKey(simpleModelName, method, null, null);
		PageElementsHolder pageElementsHolder = cache.get(key);
		if (pageElementsHolder == null || !pageElementsHolder.valid) {
			pageElementsHolder = initPageElementByModelMethod(invocation, simpleModelName, method, true, null, null);
			if (pageElementsHolder != null) {
    			cache.put(key, pageElementsHolder);
			}
		}
		return pageElementsHolder.pageElements;
	}
	
	// reset className+method and all the relevant template (remove this method if template is deprecated)
	public void reset(String className, String method) {
		String simpleModelName = beanIntrospector.getSimpleName(className);
		String checkKey1 = getKey(simpleModelName, method, null, null);
		if (!method.startsWith(FrameConstant.ACTION_PREPARE)) {
			method = FrameConstant.ACTION_PREPARE + method;
		} else {
			method = method.substring(1);
		}
		String checkKey2 = getKey(simpleModelName, method, null, null);
		
		List<String> matchedKeys = new ArrayList<String>();
		for (String key : cache.keySet()) {
			if (key.startsWith(checkKey1) || key.startsWith(checkKey2)) {
				matchedKeys.add(key);
			}
		}
		
		for (String key : matchedKeys) {
			PageElementsHolder pageElementsHolder = cache.remove(key);
			if (pageElementsHolder != null) {
				pageElementsHolder.valid = false;
			}
		}
	}
	
	public void resetPageTemplate(String path) {
		PageElementsHolder pageElementsHolder = cache.remove(path);
		if (pageElementsHolder != null) {
			pageElementsHolder.valid = false;
		}
	}
	
	private String getKey(String simpleModelName, String method, String resultCode, String inputCode) {
		return ContextHolder.get().getSiteName() + '_' + FrameConstant.STACK_PAGEELEMENTS
				+ '_' + simpleModelName + '_' + method  + (inputCode == null ? "" : inputCode);
	}
	
	private PageElements getPageElementsByPath(ActionInvocation invocation, String key, String context,
			Executor<String, Object> path, boolean autoCreation, String resultCode, String inputCode) {
		Map<String, Object> session = invocation.getInvocationContext().getSession();
    	PageElementsRef pageElementsRef = (PageElementsRef) session.get(key);
    	if (functions.isDebug()) {		// Enable it in the product environment?
    		if (pageElementsRef != null && pageElementsRef.ref.valid) {
    			long cacheTime = pageElementsRef.ref.cacheTime;
    			if (cacheTime != 0) {
    				boolean changed = false;
    				// check the modify time of the ftl file
    				String realLocation = config.getRealWebRootPath() + pageElementsRef.ref.location;
    				if (fileUtil.isChanged(realLocation, false)) {
    					changed = true;
    				} else {
    					if (fileUtil.isChanged(pageElementsRef.ref.location, true)) {
        					changed = true;
        				} else if (taglibManager.lastModifyTime() > cacheTime) {
        					changed = true;
        				} else {
        					String propertyFile = '/' + ContextHolder.get().getSiteName() + "-pageElement.properties";
        					changed = fileUtil.isChanged(propertyFile, true);
        				}
    				}
	    			
	    			if (changed) {
	    				pageElementsRef = null;
						cache.remove(key);
	    			}
    			}
    		}
    	}
    	
    	if (pageElementsRef == null || !pageElementsRef.ref.valid) {
    		PageElementsHolder pageElementsHolder = cache.get(key);
    		if ((pageElementsHolder == null || !pageElementsHolder.valid) && autoCreation) {
    			pageElementsHolder = initPageElementByPath(invocation, context, path.execute(null), resultCode, inputCode);
    			if (pageElementsHolder != null) {
	    			cache.put(key, pageElementsHolder);
    			}
    		}

    		if (pageElementsHolder != null && pageElementsHolder.valid) {
	    		PageElements pageElements = pageElementsHolder.pageElements.clone();
	    		String name = invocation.getProxy().getActionName();
	    		if (name == null) {
	    			name = context;		// for the the JUnit test case
	    		}
	        	Invoker invoker = routing.path2Invoker(name, true);
				filterReadPermission(invoker.actionName, invoker.methodName, pageElements);
				pageElementsRef = new PageElementsRef(pageElements, pageElementsHolder);
				session.put(key, pageElementsRef);
    		}
    	}
		if (pageElementsRef == null) {
			return null;
		} else {
			return pageElementsRef.pageElements;
		}
	}
	
	private PageElementsHolder initPageElementByModelMethod(ActionInvocation invocation, String simpleModelName, String method,
			boolean mustInputMethod, String resultCode, String inputCode) {
		String locationArg = getPathByModelMethod(simpleModelName, method, mustInputMethod);
		String context = Routing.invoker2PathNoExtension(simpleModelName, '.', method, true);
		return initPageElementByPath(invocation, context, locationArg, resultCode, inputCode);
	}
	
	private String getPathByModelMethod(String simpleModelName, String method, boolean mustInputMethod) {
		// call template and populate the parameter
		String actionName = beanIntrospector.getActionName(simpleModelName + '.' + method);
		Invoker invoker = new Invoker(actionName, '.', method, ContextHolder.get().getSiteName());
		// !StringUtils.isEmpty(template) --> simpleModelName and method are invalid if template exists
		String origMethod = null;
		if (mustInputMethod && !invoker.isInputMethod()) {
			origMethod = invoker.methodName;
			invoker.methodName = invoker.getInputMethod();
		}
		String context = routing.invoker2PathNoExtension(invoker, true);
		String locationArg = freemarkerTplManager.findLocation(context, invoker);
		// page is not found, try to load the original invoker page
		if (origMethod != null && freemarkerTplManager.UNDEFINED_PAGE.equals(locationArg)) {
			invoker.methodName = origMethod;
			context = routing.invoker2PathNoExtension(invoker, true);
			locationArg = freemarkerTplManager.findLocation(context, invoker);
		}
		return locationArg;
	}
	
	private PageElementsHolder initPageElementByPath(ActionInvocation invocation, String context, String path,
			String resultCode, String inputCode) {
		if (context != null) {
			boolean onlyUseInitPageElement = false;
			if (freemarkerTplManager.UNDEFINED_PAGE.equals(path) || path.endsWith(".jsp")) {
				// undefined page --> no page template
				onlyUseInitPageElement = true;
			} else {
				String method = context.substring(context.lastIndexOf('.') + 1);
				// search share the context with "_search" ("generic.search" calls "_search")
				if (resultCode != null && !"search".equals(method)) {
					// common view --> no page template
					if (resultCode.equals("$json") && "_update".equals(method)){
						onlyUseInitPageElement = false;
					} else {
						onlyUseInitPageElement = true;
					}
				} else {
					if (FrameConstant.PARAM_INPUT_JSON.equals(inputCode)) {
						// json parameter --> no page template
						onlyUseInitPageElement = true;
					}
				}
			}
			
			if (onlyUseInitPageElement) {
				PageElements pageElements = new PageElements();
				addInitPageElementField(context, pageElements);
				return new PageElementsHolder(path, pageElements);
			}
		}
		
		// Run the template and get the PageElements
		Map<String, Object> invocationContext = invocation.getStack().getContext();
		PageElements oldPageElements = (PageElements) invocationContext.get(FrameConstant.STACK_PAGEELEMENTS);
		String oldActionName = null;
		try {
			PageElements pageElements = new PageElements();
			// create an virtual environment to populate pageElements
			invocationContext.put(FrameConstant.STACK_PAGEELEMENTS, pageElements);
			String actionName = context == null ? null : routing.path2Invoker(context, true).actionName;
			oldActionName = (String)invocationContext.put(FrameConstant.STACK_ACTIONNAME, actionName);
			if (freemarkerManager == null) {
				objectFactory.injectInternalBeans(this);
			}
			
			// TODO: use FreemarkerTplManager.executeTpl();
			Configuration configuration = freemarkerManager.getConfiguration(ServletActionContext.getServletContext());
			Template templateInstance = configuration.getTemplate(path, Locale.ENGLISH);
			
			ScopesHashModel model = createModel(invocation, configuration);
			if (context != null) {
				model.put(FrameConstant.STACK_ACTIONPATH, Routing.getRequestPathNoSite(context));
			}
			model.put(FrameConstant.STACK_FUNCTION, invocationContext.get(FrameConstant.STACK_FUNCTION));
			model.put(FrameConstant.STACK_PAGEELEMENTS, pageElements);
			if (model.get("action") == null) {
				model.put("action", new BaseActionSupport());
			}
			
			CharArrayWriter parentCharArrayWriter = new CharArrayWriter();
			Environment env = templateInstance.createProcessingEnvironment(model, parentCharArrayWriter);
			//env.setClassicCompatible(true); NOTICE: the <#include> flag has issue
			env.process();
			
			if (context != null) {
				addInitPageElementField(context, pageElements);
			}
			
			pageElements.safeContext().freeze();
			return new PageElementsHolder(path, pageElements);
		} catch (Exception ex) {
			logger.error("Failed to populate the elements", ex);
		} finally {
			invocationContext.put(FrameConstant.STACK_PAGEELEMENTS, oldPageElements);
			invocationContext.put(FrameConstant.STACK_ACTIONNAME, oldActionName);
		}
		return null;
	}
	
	private void addInitPageElementField(String actionPath, PageElements ps) {
		if (pageElementProperties == null) {
			CacheService cacheService = ObjectFactory.getInstance().getBean(CacheService.class, true);
			pageElementProperties = cacheService.newVar(Properties.class);
		}
		
		int begin = actionPath.indexOf('/');
		actionPath = actionPath.substring(begin + 1);		// skip the first site name
		
		Properties properties = pageElementProperties.get();
		if (properties == null) {
			String propertyFile = '/' + ContextHolder.get().getSiteName() + "-pageElement.properties";
			InputStream input = null;
			try {
				input = this.getClass().getResourceAsStream(propertyFile);
				properties = new Properties();
				if (input != null) {
					properties.load(input);
				}
				pageElementProperties.set(properties);
			} catch (Exception ex) {
				logger.error("Failed to load property file: " + propertyFile, ex);
			} finally {
				IOUtils.closeQuietly(input);
			}
		}
		
		String property = properties == null ? null : properties.getProperty(actionPath);
		if (!StringUtils.isEmpty(property)) {
			for (String prop : StringUtils.split(property, ',')) {
				ps.addExtraName(prop, null, null);
			}
		}
	}
	
	// check R/W permission and set the permission flag(R/W) to pageElement.extraParameters.fr_sec_field
	public void filterReadPermission(String actionName, String methodName, PageElements elements) {
		UserInfo userInfo = safeManager.getLoginUser(ServletActionContext.getRequest());
		if (userInfo == null) {
			return ;
		}
		
		String userId = userInfo.getLoginId();
		String modelName = beanIntrospector.getModelName(beanIntrospector.getSimpleName(actionName));
		String path = functions.linkModelNoBase(modelName, methodName, true);
		List<String> hiddenFields = safeManager.listLimitedFields(userId, path, null, false);
		if (hiddenFields != null) {
			for (String fieldName : hiddenFields) {
				elements.setElementVisible(fieldName, false);
			}
		}
		List<String> readonlyFields = safeManager.listLimitedFields(userId, path, null, true);
		if (readonlyFields != null) {
			for (String fieldName : readonlyFields) {
				elements.setElementReadOnly(fieldName, true);
			}
		}
	}
	
	protected ScopesHashModel createModel(ActionInvocation invocation, Configuration configuration) throws TemplateModelException {
        ServletContext servletContext = ServletActionContext.getServletContext();
        HttpServletRequest request = ServletActionContext.getRequest();
        HttpServletResponse response = ServletActionContext.getResponse();
        ValueStack stack = ServletActionContext.getContext().getValueStack();

        Object action = null;
        if(invocation!= null ) {
        	action = invocation.getAction(); //Added for NullPointException
        	if (action instanceof DynamicAction) {
        		DynamicAction baseAction = ((DynamicAction)action);
        		if (baseAction.getActionName() == null) {		// check name and avoid to override the existed values
        			baseAction.prepareParameter(invocation);
        		}
        		
        		// TODO: use a better way to mock the result
        		ServiceInfo serviceInfo = baseAction.getServiceInfo();
        		if (serviceInfo != null) {
	        		Method method = serviceInfo.serviceMethod;
	        		if (method != null) {
	        			Class clazz = method.getReturnType();
	        			if (!void.class.equals(clazz) && action instanceof Map) {
		        			Object result = invocationUtil.getInitValueByParameterType(clazz.getName(), clazz, null);
		        			if (result != null) {
		        				((Map)baseAction).put("execute", result);
		        			}
	        			}
	        		}
        		}
        	}
        }
        return freemarkerManager.buildTemplateModel(stack, action, servletContext, request, response, configuration.getObjectWrapper());
    }
}
